package com.linb.pan.storage.engine.oss;

import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.*;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.linb.pan.core.constants.BPanConstants;
import com.linb.pan.core.exception.BPanFrameworkException;
import com.linb.pan.core.utils.FileUtil;
import com.linb.pan.core.utils.UUIDUtil;
import com.linb.pan.lock.core.annotation.Lock;
import com.linb.pan.storage.engine.core.AbstractStorageEngine;
import com.linb.pan.storage.engine.core.context.*;
import com.linb.pan.storage.engine.oss.config.OssStorageEngineConfig;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Component
@Slf4j
public class OSSStorageEngine extends AbstractStorageEngine {

    @Autowired
    private OssStorageEngineConfig config;

    @Autowired
    private OSSClient client;

    private static final Integer THE_THOUSAND_INT = 10000;

    private static final String CACHE_KEY_TEMPLATE = "oss_cache_upload_id_%s_%s";

    private static final String IDENTIFIER_KEY = "identifier";

    private static final String UPLOAD_ID_KEY = "uploadId";

    private static final String USER_ID_KEY = "userId";

    private static final String PART_NUMBER_KEY = "partNumber";

    private static final String E_TAG_KEY = "eTag";

    private static final String PART_SIZE_KEY = "partSize";

    private static final String PART_CRC_KEY = "partCRC";

    /**
     * 单文件上传
     */
    @Override
    protected void doStore(StoreFileContext context) throws IOException {
        String filePath = getFilePath(FileUtil.getFileSuffix(context.getFilename()));
        client.putObject(config.getBucketName(), filePath, context.getInputStream());
        context.setRealPath(filePath);
    }

    /**
     * 分片上传
     */
    @Override
    @Lock(name = "ossDoStoreChunk", keys = {"#context.userId", "context.identifier"}, expireSecond = 10L)
    protected void doStoreChunk(StoreFileChunkContext context) throws IOException {
        //1.文件分片数不能大于10000
        if (context.getTotalChunks() > THE_THOUSAND_INT) {
            throw new BPanFrameworkException("分片数超过了限制，不得大于: " + THE_THOUSAND_INT);
        }
        //2.获取缓存key
        String cacheKey = getCacheKey(context.getIdentifier(), context.getUserId());
        log.info("文件分片上传-缓存key: {}", cacheKey);
        //3.通过缓存key获取实体对象
        ChunkUploadEntity entity = getCache().get(cacheKey, ChunkUploadEntity.class);
        //4.如果获取不到就初始化实体对象
        if (entity == null) {
            entity = initChunkUpload(context.getFilename(), cacheKey);
        }
        //5.分片上传操作
        UploadPartRequest request = new UploadPartRequest();
        request.setBucketName(config.getBucketName());
        request.setKey(entity.getObjectKey());
        request.setUploadId(entity.getUploadId());
        request.setInputStream(context.getInputStream());
        request.setPartSize(context.getCurrentChunkSize());
        request.setPartNumber(context.getChunkNumber());
        UploadPartResult result = client.uploadPart(request);
        if (result == null) {
            throw new IOException("文件分片上传失败");
        } else {
            log.info("分片上传成功!");
        }
        //6.封装为url后填充到上下文
        PartETag partETag = result.getPartETag();
        JSONObject params = new JSONObject();
        params.put(IDENTIFIER_KEY, context.getIdentifier());
        params.put(UPLOAD_ID_KEY, entity.getUploadId());
        params.put(USER_ID_KEY, context.getUserId());
        params.put(PART_NUMBER_KEY, partETag.getPartNumber());
        params.put(E_TAG_KEY, partETag.getETag());
        params.put(PART_SIZE_KEY, partETag.getPartSize());
        params.put(PART_CRC_KEY, partETag.getPartCRC());
        log.info("文件分片上传-params:{}", params);
        String realPath = assembleUrl(entity.getObjectKey(), params);
        log.info("文件分片上传-封装后的url:{}", realPath);
        context.setRealPath(realPath);
    }

    /**
     * 读取文件内容并写入到输出流中
     */
    @Override
    protected void doReadFile(ReadFileContext context) throws IOException {
        OSSObject ossObject = client.getObject(config.getBucketName(), context.getRealPath());
        if (ossObject == null) {
            throw new BPanFrameworkException("文件读取失败，文件的名称为：" + context.getRealPath());
        }
        FileUtil.writeStream2StreamNormal(ossObject.getObjectContent(), context.getOutputStream());
    }

    /**
     * 合并文件分片
     */
    @Override
    protected void doMergeFile(MergeFileContext context) throws IOException {
        //1.获取缓存信息，拿到全局的uploadId
        String cacheKey = getCacheKey(context.getIdentifier(), context.getUserId());
        ChunkUploadEntity entity = getCache().get(cacheKey, ChunkUploadEntity.class);
        log.info("合并文件分片-拿到ChunkUploadEntity对象:{}", entity);
        if (entity == null) {
            throw new BPanFrameworkException("文件分片合并失败，文件的唯一标识为：" + context.getIdentifier());
        }
        //2.从上下文信息获取所有分片的url，解析出要执行文件合并请求的参数
        List<String> chunkPaths = context.getRealPathList();
        List<PartETag> partETags = Lists.newArrayList();
        if (CollectionUtils.isNotEmpty(chunkPaths)) {
            partETags = chunkPaths.stream()
                    .filter(StringUtils::isNotBlank)
                    .map(this::analysisUrlParams)
                    .filter(Objects::nonNull)
                    .filter(jsonObject -> !jsonObject.isEmpty())
                    .map(jsonObject -> new PartETag(jsonObject.getIntValue(PART_NUMBER_KEY),
                            jsonObject.getString(E_TAG_KEY),
                            jsonObject.getLongValue(PART_SIZE_KEY),
                            jsonObject.getLong(PART_CRC_KEY)
                    )).collect(Collectors.toList());
        }
        //3.执行文件合并的请求
        CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest(config.getBucketName(), entity.getObjectKey(), entity.uploadId, partETags);
        CompleteMultipartUploadResult result = client.completeMultipartUpload(request);
        if (result == null) {
            throw new BPanFrameworkException("文件分片合并失败，文件的唯一标识为：" + context.getIdentifier());
        }
        //4.清除缓存
        getCache().evict(cacheKey);
        //5.设置合并后的实际路径
        String realPath = entity.getObjectKey();
        context.setRealPath(realPath);
    }

    /**
     * 删除文件 分为普通文件删除 和 分片文件删除
     */
    @Override
    protected void doDelete(DeleteFileContext context) {
        List<String> realFilePathList = context.getRealFilePathList();
        log.info("要删除的路径:{}", realFilePathList);
        realFilePathList.stream().forEach(realPath -> {
            //分片的情况
            if (checkHaveParams(realPath)) {
                JSONObject params = analysisUrlParams(realPath);
                if (params != null && !params.isEmpty()) {
                    String uploadId = params.getString(UPLOAD_ID_KEY);
                    String identifier = params.getString(IDENTIFIER_KEY);
                    Long userId = params.getLong(USER_ID_KEY);
                    String cacheKey = getCacheKey(identifier, userId);
                    getCache().evict(cacheKey);
                    try {
                        AbortMultipartUploadRequest request = new AbortMultipartUploadRequest(config.getBucketName(), getBaseUrl(realPath), uploadId);
                        client.abortMultipartUpload(request);
                        log.info("分片文件删除");
                    } catch (Exception e) {
                        log.error("文件删除失败");
                    }
                }
            } else {
                client.deleteObject(config.getBucketName(), realPath);
                log.info("普通文件删除");
            }
        });
    }

    /*==================================private==================================*/

    /**
     * 获取对象的完整名称 年/月/日/UUID.suffix
     */
    private String getFilePath(String suffix) {
        return new StringBuffer()
                .append(DateUtil.thisYear())
                .append(BPanConstants.SLASH_STR)
                .append(DateUtil.thisMonth() + 1)
                .append(BPanConstants.SLASH_STR)
                .append(DateUtil.thisDayOfMonth())
                .append(BPanConstants.SLASH_STR)
                .append(UUIDUtil.getUUID())
                .append(suffix)
                .toString();
    }

    /**
     * 文件分片上传-获取缓存key
     */
    private String getCacheKey(String identifier, Long userId) {
        return String.format(CACHE_KEY_TEMPLATE, identifier, userId);
    }

    /**
     * 分片上传对象
     */
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @EqualsAndHashCode
    @ToString
    public static class ChunkUploadEntity implements Serializable {
        private static final long serialVersionUID = 7490213987137595800L;
        private String uploadId; //分片上传全局唯一uploadId
        private String objectKey; //分片上传实体名称
    }

    /**
     * 初始化文件分片上传
     */
    private ChunkUploadEntity initChunkUpload(String filename, String cacheKey) {
        String filepath = getFilePath(filename);
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(config.getBucketName(), filepath);
        InitiateMultipartUploadResult result = client.initiateMultipartUpload(request);
        if (result == null) {
            throw new BPanFrameworkException("文件分片上传初始化失败");
        }
        ChunkUploadEntity entity = new ChunkUploadEntity();
        entity.setUploadId(result.getUploadId());
        entity.setObjectKey(filepath);
        log.info("初始化文件分片上传 uploadId:{} objectKey:{}", result.getUploadId(), filepath);
        getCache().put(cacheKey, entity);
        return entity;
    }

    /**
     * 封装url
     */
    private String assembleUrl(String baseUrl, JSONObject params) {
        if (Objects.isNull(params) || params.isEmpty()) {
            return baseUrl;
        }
        StringBuffer urlStringBuffer = new StringBuffer(baseUrl);
        urlStringBuffer.append(BPanConstants.QUESTION_MARK_STR);
        List<String> paramsList = Lists.newArrayList();
        StringBuffer urlParamsStringBuffer = new StringBuffer();
        params.entrySet().forEach(entry -> {
            urlParamsStringBuffer.setLength(BPanConstants.ZERO_INT);
            urlParamsStringBuffer.append(entry.getKey());
            urlParamsStringBuffer.append(BPanConstants.EQUALS_MARK_STR);
            urlParamsStringBuffer.append(entry.getValue());
            paramsList.add(urlParamsStringBuffer.toString());
        });
        return urlStringBuffer.append(Joiner.on(BPanConstants.AND_MARK_STR).join(paramsList)).toString();
    }

    /**
     * 获取基础url
     */
    private String getBaseUrl(String url) {
        if (StringUtils.isBlank(url)) {
            return BPanConstants.EMPTY_STR;
        }
        if (checkHaveParams(url)) {
            return url.split(getSplitMark(BPanConstants.QUESTION_MARK_STR))[0];
        }
        return url;
    }

    /**
     * 获取截取字符串的关键标识
     * 由于java的字符串分割会按照正则去截取
     * 我们的URL会影响标识的识别，故添加左右中括号去分组
     */
    private String getSplitMark(String mark) {
        String s = new StringBuffer(BPanConstants.LEFT_BRACKET_STR)
                .append(mark)
                .append(BPanConstants.RIGHT_BRACKET_STR)
                .toString();
        log.info("文件分片上传-截取字符串的关键标识:{}", s);
        return s;
    }

    /**
     * 分析url参数
     */
    private JSONObject analysisUrlParams(String url) {
        JSONObject result = new JSONObject();
        if (!checkHaveParams(url)) {
            return result;
        }
        String paramsPart = url.split(getSplitMark(BPanConstants.QUESTION_MARK_STR))[1];
        if (StringUtils.isNotBlank(paramsPart)) {
            List<String> paramPairList = Splitter.on(BPanConstants.AND_MARK_STR).splitToList(paramsPart);
            paramPairList.stream().forEach(paramPair -> {
                String[] paramArr = paramPair.split(getSplitMark(BPanConstants.EQUALS_MARK_STR));
                if (paramArr != null && paramArr.length == BPanConstants.TWO_INT) {
                    result.put(paramArr[0], paramArr[1]);
                }
            });
        }
        log.info("解析后的url:", result);
        return result;
    }

    /**
     * 检查是否是含有参数的URL
     */
    private boolean checkHaveParams(String url) {
        log.info("文件分片上传-检查是否含有参数的url: {}, {}", url.indexOf(BPanConstants.QUESTION_MARK_STR), BPanConstants.MINUS_ONE_INT);
        return StringUtils.isNotBlank(url) && url.indexOf(BPanConstants.QUESTION_MARK_STR) != BPanConstants.MINUS_ONE_INT;
    }
}
